package be.novelfaces.component.util;

import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.el.ValueExpression;
import javax.faces.FacesException;
import javax.faces.component.ActionSource;
import javax.faces.component.ActionSource2;
import javax.faces.component.EditableValueHolder;
import javax.faces.component.UIComponent;
import javax.faces.component.UIOutput;
import javax.faces.component.UIViewRoot;
import javax.faces.component.ValueHolder;
import javax.faces.component.behavior.ClientBehavior;
import javax.faces.component.behavior.ClientBehaviorContext;
import javax.faces.component.behavior.ClientBehaviorContext.Parameter;
import javax.faces.component.behavior.ClientBehaviorHint;
import javax.faces.component.behavior.ClientBehaviorHolder;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.convert.Converter;
import javax.faces.convert.ConverterException;

import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;

import be.novelfaces.component.util.attribute.PassThroughAttribute;
import be.novelfaces.component.util.attribute.PassThroughAttributes;

class RenderUtilsImpl implements RenderUtils {

	static final String PERVENT_DEFAULT_SCRIPT = ";return false";
	public static final String JAVAX_FACES_SOURCE = "javax.faces.source";
	public static final String JAVAX_FACES_BEHAVIOR_EVENT = "javax.faces.behavior.event";

	public static final String NON_SUBMITTED_VALUE_WARNING = "There should always be a submitted value for an input if it is rendered,"
			+ " its form is submitted, and it was not originally rendered disabled or read-only."
			+ "  You cannot submit a form after disabling an input element via javascript."
			+ "  Consider setting read-only to true instead"
			+ " or resetting the disabled value back to false prior to form submission.";

	private static final Logger log = Logger.getLogger(RenderUtilsImpl.class
			.getName());

	RenderUtilsImpl() {
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * be.cegeka.jsf2.comp.dev.component.util.RenderUtils#decodeUIInput(javax
	 * .faces.context.FacesContext, javax.faces.component.UIComponent)
	 */
	@Override
	public void decodeUIInput(FacesContext facesContext, UIComponent component) {
		if (!(isEditableValueHolder(component))) {
			throw new IllegalArgumentException("Component "
					+ component.getClientId(facesContext)
					+ " is not an EditableValueHolder");
		}

		if (isDisabledOrReadOnly(component))
			return;

		Map<String, String> paramMap = facesContext.getExternalContext()
				.getRequestParameterMap();
		String clientId = component.getClientId(facesContext);

		if (paramMap.containsKey(clientId)) {
			((EditableValueHolder) component).setSubmittedValue(paramMap
					.get(clientId));
		} else {
			log.warning(NON_SUBMITTED_VALUE_WARNING + " Component : "
					+ getPathToComponent(component));
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * be.cegeka.jsf2.comp.dev.component.util.RenderUtils#decodeClientBehaviors
	 * (javax.faces.context.FacesContext, javax.faces.component.UIComponent)
	 */
	@Override
	public void decodeClientBehaviors(FacesContext facesContext,
			UIComponent component) {
		if (component instanceof ClientBehaviorHolder) {
			ClientBehaviorHolder clientBehaviorHolder = (ClientBehaviorHolder) component;

			Map<String, List<ClientBehavior>> clientBehaviors = clientBehaviorHolder
					.getClientBehaviors();

			if (clientBehaviors != null && !clientBehaviors.isEmpty()) {
				Map<String, String> paramMap = facesContext
						.getExternalContext().getRequestParameterMap();

				String behaviorEventName = paramMap
						.get(JAVAX_FACES_BEHAVIOR_EVENT);

				if (behaviorEventName != null) {
					List<ClientBehavior> clientBehaviorList = clientBehaviors
							.get(behaviorEventName);

					if (clientBehaviorList != null
							&& !clientBehaviorList.isEmpty()) {
						String clientId = paramMap.get(JAVAX_FACES_SOURCE);

						if (component.getClientId().equals(clientId)) {
							for (ClientBehavior clientBehavior : clientBehaviorList) {
								clientBehavior.decode(facesContext, component);
							}
						}
					}
				}
			}
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * be.cegeka.jsf2.comp.dev.component.util.RenderUtils#checkParamValidity
	 * (javax.faces.context.FacesContext, javax.faces.component.UIComponent,
	 * java.lang.Class)
	 */
	@Override
	public void checkParamValidity(FacesContext facesContext,
			UIComponent uiComponent, Class<? extends Object> compClass) {
		if (facesContext == null)
			throw new NullPointerException("facesContext may not be null");
		if (uiComponent == null)
			throw new NullPointerException("uiComponent may not be null");

		if (compClass != null && !(compClass.isInstance(uiComponent))) {
			throw new IllegalArgumentException("uiComponent : "
					+ getPathToComponent(uiComponent) + " is not instance of "
					+ compClass.getName() + " as it should be");
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * be.cegeka.jsf2.comp.dev.component.util.RenderUtils#isDisabledOrReadOnly
	 * (javax.faces.component.UIComponent)
	 */
	@Override
	public boolean isDisabledOrReadOnly(UIComponent component) {
		return isDisabled(component) || isReadOnly(component);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * be.cegeka.jsf2.comp.dev.component.util.RenderUtils#isDisabled(javax.faces
	 * .component.UIComponent)
	 */
	@Override
	public boolean isDisabled(UIComponent component) {
		return isTrue(component.getAttributes().get("disabled"));
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * be.cegeka.jsf2.comp.dev.component.util.RenderUtils#isReadOnly(javax.faces
	 * .component.UIComponent)
	 */
	@Override
	public boolean isReadOnly(UIComponent component) {
		return isTrue(component.getAttributes().get("readonly"));
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * be.cegeka.jsf2.comp.dev.component.util.RenderUtils#getConvertedUIOutputValue
	 * (javax.faces.context.FacesContext, javax.faces.component.UIOutput,
	 * java.lang.Object)
	 */
	@Override
	public Object getConvertedUIOutputValue(FacesContext facesContext,
			UIOutput output, Object submittedValue) throws ConverterException {
		if (submittedValue != null && !(submittedValue instanceof String)) {
			submittedValue = submittedValue.toString();
		}

		Converter converter = findUIOutputConverter(facesContext, output);

		return converter == null ? submittedValue : converter.getAsObject(
				facesContext, output, (String) submittedValue);
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * be.cegeka.jsf2.comp.dev.component.util.RenderUtils#findUIOutputConverter
	 * (javax.faces.context.FacesContext, javax.faces.component.UIOutput)
	 */
	@Override
	public Converter findUIOutputConverter(FacesContext facesContext,
			UIOutput component) {
		Converter converter = component.getConverter();
		if (converter != null) {
			return converter;
		}

		ValueExpression expression = component.getValueExpression("value");
		if (expression == null) {
			return null;
		}

		return getConverterForType(facesContext,
				(Class<?>) expression.getType(facesContext.getELContext()));
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * be.cegeka.jsf2.comp.dev.component.util.RenderUtils#getInputValue(javax
	 * .faces.context.FacesContext, javax.faces.component.UIComponent)
	 */
	@Override
	public String getStringValue(FacesContext facesContext,
			UIComponent component) throws ConverterException {
		throwExceptionIfComponentNotValueHolder(component);
		if (isEditableValueHolder(component)) {
			Object submittedValue = ((EditableValueHolder) component)
					.getSubmittedValue();
			if (submittedValue != null) {
				if (log.isLoggable(Level.FINE))
					log.fine("returning submitted value: '" + submittedValue
							+ "'");
				return submittedValue.toString();
			}
		}

		Object value = getValue(component);

		Converter converter = getConverterByValue(facesContext, component,
				value);

		if (converter != null) {
			if (log.isLoggable(Level.FINE))
				log.fine("returning converter get as string " + converter);
			return converter.getAsString(facesContext, component, value);
		} else if (value != null) {
			if (log.isLoggable(Level.FINE))
				log.fine("returning value.toString (no converter)");
			return value.toString();
		}
		if (log.isLoggable(Level.FINE))
			log.fine("returning an empty string (no converter and value null)");
		return "";
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see be.cegeka.jsf2.comp.dev.component.util.RenderUtils#
	 * renderPassThroughAttributes(javax.faces.context.FacesContext,
	 * javax.faces.context.ResponseWriter, javax.faces.component.UIComponent,
	 * be.cegeka.jsf2.comp.dev.component.util.attribute.PassThroughAttributes)
	 */
	@Override
	public void renderPassThroughAttributes(FacesContext facesContext,
			UIComponent component, PassThroughAttributes passThroughAttributes)
			throws IOException {
		Map<String, List<ClientBehavior>> behaviors = null;

		if (component instanceof ClientBehaviorHolder) {
			behaviors = ((ClientBehaviorHolder) component).getClientBehaviors();
		}
		behaviors = behaviorScriptsMayNotRenderedWhenComponentIsDisabled(
				component, behaviors);

		renderPassThroughAttributes(facesContext, component,
				passThroughAttributes, behaviors);

	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * be.cegeka.jsf2.comp.dev.component.util.RenderUtils#renderPassThruAttributes
	 * (javax.faces.context.FacesContext, javax.faces.context.ResponseWriter,
	 * javax.faces.component.UIComponent,
	 * be.cegeka.jsf2.comp.dev.component.util.attribute.PassThroughAttributes,
	 * java.util.Map)
	 */
	@Override
	public void renderPassThroughAttributes(FacesContext facesContext,
			UIComponent component, PassThroughAttributes passThroughAttributes,
			Map<String, List<ClientBehavior>> behaviors) throws IOException {
		if (behaviors == null) {
			behaviors = Collections.emptyMap();
		}

		Map<String, Object> attrMap = component.getAttributes();

		for (PassThroughAttribute attribute : passThroughAttributes) {
			String attrName = attribute.getName();
			String[] events = attribute.getEvents();

			boolean hasBehavior = hasBehavior(behaviors, events);
			Object value = attrMap.get(attrName);

			if (value != null && !hasBehavior) {
				renderPassThroughAttribute(facesContext, attribute, value);
			} else if (hasBehavior) {
				renderEventHandler(facesContext, component, null, attrName,
						value, events[0], behaviors.get(events[0]));
			}
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * be.cegeka.jsf2.comp.dev.component.util.RenderUtils#renderEnventHandler
	 * (javax.faces.context.FacesContext, javax.faces.component.UIComponent,
	 * java.util.Collection, java.lang.String, java.lang.Object,
	 * java.lang.String)
	 */
	@Override
	public void renderEventHandler(FacesContext facesContext,
			UIComponent component, Collection<Parameter> params,
			String eventHandlerName, Object eventHandlerValue,
			String behaviorEventName, List<ClientBehavior> behaviors)
			throws IOException {
		ResponseWriter writer = facesContext.getResponseWriter();
		String userHandler = getNonEmptyUserHandler(eventHandlerValue);

		behaviors = behaviorScriptsMayNotRenderedWhenComponentIsDisabled(
				component, behaviors);

		if (params == null) {
			params = Collections.emptyList();
		}

		String handler = null;
		if (isUserHandlerOnly(behaviors)) {
			handler = userHandler;
		} else if (isSingleBehaviorOnly(userHandler, behaviors)) {
			handler = getSingleBehaviorHandler(facesContext, component,
					behaviors.get(0), params, behaviorEventName);
		} else {
			handler = getChainedHandler(facesContext, component, behaviors,
					params, behaviorEventName, userHandler);
		}

		if (!isDefaultAttributeValue(handler)) {
			writer.writeAttribute(eventHandlerName, handler, null);
		}
	}

	private List<ClientBehavior> behaviorScriptsMayNotRenderedWhenComponentIsDisabled(
			UIComponent component, List<ClientBehavior> behaviors) {
		if ((null != behaviors) && (behaviors.size() > 0)
				&& isDisabled(component)) {
			behaviors = null;
		}
		return behaviors;
	}

	String getChainedHandler(FacesContext facesContext, UIComponent component,
			List<ClientBehavior> behaviors, Collection<Parameter> params,
			String behaviorEventName, String userHandler) {
		StringBuilder builder = new StringBuilder(80);
		builder.append("jsf.util.chain(this,event,");

		appendScriptToChain(builder, userHandler);

		boolean submitting = appendBehaviorsToChain(builder, facesContext,
				component, behaviors, behaviorEventName, params);

		builder.append(")");

		if (submitting && "action".equals(behaviorEventName)) {
			builder.append(PERVENT_DEFAULT_SCRIPT);
		}

		return builder.toString();
	}

	String getSingleBehaviorHandler(FacesContext facesContext,
			UIComponent component, ClientBehavior clientBehavior,
			Collection<Parameter> params, String behaviorEventName) {

		ClientBehaviorContext bContext = createClientBehaviorContext(
				facesContext, component, behaviorEventName, params);

		String script = clientBehavior.getScript(bContext);
		boolean preventDefault = (isSubmitting(clientBehavior) && isActionSourceComponent(component));

		if (script != null && preventDefault) {
			script = script + PERVENT_DEFAULT_SCRIPT;
		}

		return script;
	}

	private boolean isActionSourceComponent(UIComponent component) {
		return (component instanceof ActionSource || component instanceof ActionSource2);
	}

	private ClientBehaviorContext createClientBehaviorContext(
			FacesContext facesContext, UIComponent component,
			String behaviorEventName, Collection<Parameter> params) {
		return ClientBehaviorContext.createClientBehaviorContext(facesContext,
				component, behaviorEventName, null, params);
	}

	// Appends one or more behavior scripts a jsf.util.chain() call
	private boolean appendBehaviorsToChain(StringBuilder builder,
			FacesContext facesContext, UIComponent component,
			List<ClientBehavior> behaviors, String behaviorEventName,
			Collection<ClientBehaviorContext.Parameter> params) {

		if (isEmpty(behaviors)) {
			return false;
		}

		ClientBehaviorContext bContext = createClientBehaviorContext(
				facesContext, component, behaviorEventName, params);

		boolean submitting = false;

		for (ClientBehavior behavior : behaviors) {
			String script = behavior.getScript(bContext);
			if (StringUtils.isNotEmpty(script)) {
				appendScriptToChain(builder, script);

				if (isSubmitting(behavior)) {
					submitting = true;
				}
			}
		}

		return submitting;
	}

	private boolean isSubmitting(ClientBehavior behavior) {
		return behavior.getHints().contains(ClientBehaviorHint.SUBMITTING);
	}

	private void appendScriptToChain(StringBuilder builder, String script) {

		if (StringUtils.isEmpty(script)) {
			return;
		}

		if (builder.charAt(builder.length() - 1) != ',')
			builder.append(',');

		appendQuotedValue(builder, script);
	}

	// Append a script to the chain, escaping any single quotes, since
	// our script content is itself nested within single quotes.
	private void appendQuotedValue(StringBuilder builder, String script) {

		builder.append("'");

		int length = script.length();

		for (int i = 0; i < length; i++) {
			char c = script.charAt(i);

			if (c == '\'' || c == '\\') {
				builder.append('\\');
			}

			builder.append(c);
		}

		builder.append("'");
	}

	private boolean isSingleBehaviorOnly(String userHandler,
			List<ClientBehavior> behaviors) {
		return behaviors.size() == 1 && userHandler == null;
	}

	private boolean isUserHandlerOnly(List<ClientBehavior> behaviors) {
		return isEmpty(behaviors);
	}

	private boolean isEmpty(List<?> coll) {
		return coll == null || coll.size() == 0;
	}

	private String getNonEmptyUserHandler(Object handlerObject) {

		String handler = null;

		if (null != handlerObject) {
			handler = handlerObject.toString();
			handler = handler.trim();

			if (handler.length() == 0)
				handler = null;
		}

		return handler;
	}

	private boolean hasBehavior(Map<String, List<ClientBehavior>> behaviors,
			String[] events) {
		return (!ArrayUtils.isEmpty(events) && behaviors.containsKey(events[0]));
	}

	private Map<String, List<ClientBehavior>> behaviorScriptsMayNotRenderedWhenComponentIsDisabled(
			UIComponent component, Map<String, List<ClientBehavior>> behaviors) {
		if (isNotEmpty(behaviors) && isDisabled(component)) {
			behaviors = null;
		}
		return behaviors;
	}

	private boolean isNotEmpty(Map<?, ?> map) {
		return !(map == null || map.isEmpty());
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * be.cegeka.jsf2.comp.dev.component.util.RenderUtils#renderAttribute(javax
	 * .faces.context.FacesContext, java.lang.String, java.lang.Object)
	 */
	@Override
	public void renderPassThroughAttribute(FacesContext facesContext,
			PassThroughAttribute passThroughAttribute, Object attributeValue)
			throws IOException {
		if (isDefaultAttributeValue(attributeValue)) {
			return;
		}

		ResponseWriter writer = facesContext.getResponseWriter();

		String prefixedAttributeName = prefixAttributeName(
				passThroughAttribute.getName(), writer);

		switch (passThroughAttribute.getKind()) {
		case URI:
			writer.writeURIAttribute(prefixedAttributeName, attributeValue,
					null);
			break;
		case BOOLEAN:
			writer.writeAttribute(prefixedAttributeName, Boolean.TRUE, null);
			break;
		default:
			writer.writeAttribute(prefixedAttributeName, attributeValue, null);
			break;
		}

	}

	private String prefixAttributeName(String attributeName,
			ResponseWriter responseWriter) {
		if (XHTML_CONTENT_TYPE.equals(responseWriter.getContentType())) {
			if ("lang".equals(attributeName)) {
				return XHTML_ATTRIBUTE_PREFIX + attributeName;
			}
		}

		return attributeName;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * be.cegeka.jsf2.comp.dev.component.util.RenderUtils#isDefaultAttributeValue
	 * (java.lang.Object)
	 */
	@Override
	public boolean isDefaultAttributeValue(Object value) {
		if (value == null) {
			return true;
		} else if (value instanceof Boolean) {
			return !((Boolean) value).booleanValue();
		} else if (value instanceof Number) {
			if (value instanceof Integer) {
				return ((Number) value).intValue() == Integer.MIN_VALUE;
			} else if (value instanceof Double) {
				return ((Number) value).doubleValue() == Double.MIN_VALUE;
			} else if (value instanceof Long) {
				return ((Number) value).longValue() == Long.MIN_VALUE;
			} else if (value instanceof Byte) {
				return ((Number) value).byteValue() == Byte.MIN_VALUE;
			} else if (value instanceof Float) {
				return ((Number) value).floatValue() == Float.MIN_VALUE;
			} else if (value instanceof Short) {
				return ((Number) value).shortValue() == Short.MIN_VALUE;
			}
		}
		return false;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * be.cegeka.jsf2.comp.dev.component.util.RenderUtils#getPathToComponent
	 * (javax.faces.component.UIComponent)
	 */
	@Override
	public String getPathToComponent(UIComponent component) {
		StringBuffer buf = new StringBuffer();

		if (component == null) {
			buf.append("{Component-Path : ");
			buf.append("[null]}");
			return buf.toString();
		}

		getPathToComponent(component, buf);

		buf.insert(0, "{Component-Path : ");
		buf.append("}");

		return buf.toString();
	}

	Converter getConverterForType(FacesContext facesContext, Class<?> valueType) {
		if (valueType == null) {
			return null;
		}

		if (Object.class.equals(valueType)) {
			return null; // There is no converter for Object class
		}
		try {
			return facesContext.getApplication().createConverter(valueType);
		} catch (FacesException e) {
			log.log(Level.SEVERE,
					"No Converter for type " + valueType.getName() + " found",
					e);
			return null;
		}
	}

	Converter getConverterByValue(FacesContext facesContext,
			UIComponent component, Object value) {
		Converter converter = ((ValueHolder) component).getConverter();
		if (converter == null && value != null) {
			converter = getConverterForType(facesContext, value.getClass());
		}
		return converter;
	}

	Object getValue(UIComponent component) {
		if (isEditableValueHolder(component)) {
			EditableValueHolder holder = (EditableValueHolder) component;
			if (holder.isLocalValueSet()) {
				return holder.getLocalValue();
			}
		}
		return ((ValueHolder) component).getValue();
	}

	private boolean isEditableValueHolder(UIComponent component) {
		return component instanceof EditableValueHolder;
	}

	private void throwExceptionIfComponentNotValueHolder(UIComponent component) {
		if (!isValueHolder(component)) {
			throw new IllegalArgumentException("Component : "
					+ getPathToComponent(component) + "is not a ValueHolder");
		}
	}

	private boolean isValueHolder(UIComponent component) {
		return component instanceof ValueHolder;
	}

	private void getPathToComponent(UIComponent component, StringBuffer buf) {
		if (component == null)
			return;

		StringBuffer intBuf = new StringBuffer();

		intBuf.append("[Class: ");
		intBuf.append(component.getClass().getName());
		if (component instanceof UIViewRoot) {
			intBuf.append(",ViewId: ");
			intBuf.append(((UIViewRoot) component).getViewId());
		} else {
			intBuf.append(",Id: ");
			intBuf.append(component.getId());
		}
		intBuf.append("]");

		buf.insert(0, intBuf.toString());

		getPathToComponent(component.getParent(), buf);
	}

	private boolean isTrue(Object obj) {
		if (obj instanceof String) {
			return new Boolean((String) obj);
		}

		if (!(obj instanceof Boolean))
			return false;

		return ((Boolean) obj).booleanValue();
	}

}
